"""
Base classes and data structures for device connectors
"""

from __future__ import annotations

import asyncio
import logging
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from typing import Any


class DataQuality(str, Enum):
    """Data quality enumeration"""

    GOOD = "good"
    BAD = "bad"
    UNCERTAIN = "uncertain"


class ConnectionStatus(str, Enum):
    """Connection status enumeration"""

    DISCONNECTED = "disconnected"
    CONNECTING = "connecting"
    CONNECTED = "connected"
    ERROR = "error"


@dataclass
class DataPoint:
    """
    Unified data point structure for all connectors

    Attributes:
        value: The data value (can be any type)
        timestamp: When the data was collected
        quality: Data quality indicator
        address: Data point address/identifier
        unit: Measurement unit (optional)
        metadata: Additional metadata (optional)
    """

    value: Any
    timestamp: datetime
    quality: DataQuality = DataQuality.GOOD
    address: str | None = None
    unit: str | None = None
    metadata: dict[str, Any] = field(default_factory=dict)

    def to_dict(self) -> dict[str, Any]:
        """Convert data point to dictionary"""
        return {
            "value": self.value,
            "timestamp": self.timestamp.isoformat(),
            "quality": self.quality.value,
            "address": self.address,
            "unit": self.unit,
            "metadata": self.metadata,
        }

    @classmethod
    def from_dict(cls, data: dict[str, Any]) -> DataPoint:
        """Create data point from dictionary"""
        return cls(
            value=data["value"],
            timestamp=datetime.fromisoformat(data["timestamp"]),
            quality=DataQuality(data.get("quality", "good")),
            address=data.get("address"),
            unit=data.get("unit"),
            metadata=data.get("metadata", {}),
        )


class BaseConnector(ABC):
    """
    Abstract base class for all device connectors

    Provides common functionality for connecting to devices,
    reading/writing data, and managing connection lifecycle.

    All concrete connector implementations must inherit from this class
    and implement the abstract methods.
    """

    def __init__(self, config: dict[str, Any]) -> None:
        """
        Initialize connector with configuration

        Args:
            config: Connector configuration dictionary
                Required keys:
                    - device_id: Unique device identifier
                Optional keys:
                    - name: Device name
                    - timeout: Connection timeout in seconds (default: 5)
                    - retry_count: Number of retries for failed operations (default: 3)
                    - retry_delay: Delay between retries in seconds (default: 1)
        """
        self.config = config
        self.device_id: int = config["device_id"]  # Required field
        self.name: str = config.get("name", "Unknown Device")
        self.status: ConnectionStatus = ConnectionStatus.DISCONNECTED
        self.timeout: int = config.get("timeout", 5)
        self.retry_count: int = config.get("retry_count", 3)
        self.retry_delay: float = config.get("retry_delay", 1.0)
        self.logger = logging.getLogger(f"connector.{self.name}")

        # Validate configuration
        self._validate_config()

    def _validate_config(self) -> None:
        """
        Validate connector configuration

        Raises:
            ValueError: If required configuration is missing or invalid
        """
        if not self.device_id:
            raise ValueError("device_id is required in connector configuration")

        if self.timeout <= 0:
            raise ValueError(f"timeout must be positive, got {self.timeout}")

        if self.retry_count < 0:
            raise ValueError(f"retry_count must be non-negative, got {self.retry_count}")

        if self.retry_delay < 0:
            raise ValueError(f"retry_delay must be non-negative, got {self.retry_delay}")

    @abstractmethod
    async def connect(self) -> bool:
        """
        Establish connection to the device

        Returns:
            bool: True if connection successful, False otherwise

        Raises:
            ConnectionError: If connection fails
        """
        ...

    @abstractmethod
    async def disconnect(self) -> bool:
        """
        Disconnect from the device

        Returns:
            bool: True if disconnection successful, False otherwise
        """
        ...

    @abstractmethod
    async def read_single(self, address: str) -> DataPoint:
        """
        Read a single data point from the device

        Args:
            address: Address/identifier of the data point to read

        Returns:
            DataPoint: The read data point

        Raises:
            ReadError: If read operation fails
        """
        ...

    @abstractmethod
    async def read_multiple(self, addresses: list[str]) -> list[DataPoint]:
        """
        Read multiple data points from the device

        Args:
            addresses: List of addresses to read

        Returns:
            List[DataPoint]: List of read data points

        Raises:
            ReadError: If read operation fails
        """
        ...

    @abstractmethod
    async def write_single(self, address: str, value: Any) -> bool:
        """
        Write a single value to the device

        Args:
            address: Address/identifier of the data point to write
            value: Value to write

        Returns:
            bool: True if write successful, False otherwise

        Raises:
            WriteError: If write operation fails
        """
        ...

    async def write_multiple(self, data: dict[str, Any]) -> bool:
        """
        Write multiple values to the device

        Default implementation writes values sequentially.
        Subclasses can override for batch write optimization.

        Args:
            data: Dictionary of {address: value} pairs

        Returns:
            bool: True if all writes successful, False otherwise

        Raises:
            WriteError: If any write operation fails
        """
        results: list[bool] = []
        for address, value in data.items():
            result = await self.write_single(address, value)
            results.append(result)

        return all(results)

    async def health_check(self) -> bool:
        """
        Perform health check on the connection

        Default implementation checks connection status.
        Subclasses can override for more sophisticated checks.

        Returns:
            bool: True if connection is healthy, False otherwise
        """
        return self.status == ConnectionStatus.CONNECTED

    async def _retry_operation(
        self,
        operation: Any,
        *args: Any,
        **kwargs: Any
    ) -> Any:
        """
        Retry an operation with exponential backoff

        Args:
            operation: Async function to retry
            *args: Positional arguments for the operation
            **kwargs: Keyword arguments for the operation

        Returns:
            Result of the operation

        Raises:
            Exception: The last exception if all retries fail
        """
        last_exception = None

        for attempt in range(self.retry_count + 1):
            try:
                return await operation(*args, **kwargs)
            except Exception as e:
                last_exception = e
                if attempt < self.retry_count:
                    delay = self.retry_delay * (2 ** attempt)  # Exponential backoff
                    self.logger.warning(
                        f"Operation failed (attempt {attempt + 1}/{self.retry_count + 1}): {e}. "
                        f"Retrying in {delay}s..."
                    )
                    await asyncio.sleep(delay)
                else:
                    self.logger.error(
                        f"Operation failed after {self.retry_count + 1} attempts: {e}"
                    )

        if last_exception:
            raise last_exception

        raise RuntimeError("Retry operation failed without exception")

    async def __aenter__(self) -> BaseConnector:
        """
        Async context manager entry

        Automatically connects to the device when entering context.

        Returns:
            self: The connector instance
        """
        await self.connect()
        return self

    async def __aexit__(
        self,
        exc_type: type | None,
        exc_val: BaseException | None,
        exc_tb: Any | None
    ) -> None:
        """
        Async context manager exit

        Automatically disconnects from the device when exiting context.
        """
        await self.disconnect()
